<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
    }

    #canvas {
      background-color: antiquewhite;
    }
  </style>
</head>

<body>
  <canvas id="canvas"></canvas>
  <script>
    /** @type {HTMLCanvasElement} */
    const canvas = document.querySelector('#canvas');
    const [width, height] = [window.innerWidth, window.innerHeight];
    canvas.width = width;
    canvas.height = height;
    const ctx = canvas.getContext('2d');

    // 粒子尺寸
    const partSize = 24;
    // 所有粒子的边界
    const edge = { left: 0, right: width, bottom: height - 50 };

    class Particle {
      constructor(width, height) {
        this.width = width;
        this.height = height;
        // 位置
        this.x = 0;
        this.y = 0;
        // 1: 新生 0: 坠落
        this.state = 1;
        this.parent = null;

        // 速度
        this.vx = this.getVx();
        this.vy = 0.002;
        // 重力
        this.gravity = 0.03;
        // 弹力
        this.bounce = 0.85;
      }
      /**
       * 获取X轴的速度, 避免直上直下的弹动
       * vx取值的范围是[-0.5,0.5]但不能在[-0.15,0.15]之间
      */
      getVx() {
        let vx = Math.random() - 0.5;
        if (Math.abs(vx) < 0.15) {
          return this.getVx()
        } else {
          return vx;
        }
      }
      /**
       * 更新数据
       * diff以毫秒为单位的时间差
      */
      update(diff) {
        // 解构状态、尺寸和位置
        const { state, width, parent, gravity, bounce } = this;
        // 解构边界
        const { left, right, bottom } = edge;
        if (!state) {
          // 让粒子的y轴速度加上重力
          this.vy += gravity;
          // 设置粒子位置
          this.y += this.vy*diff;
          this.x += this.vx*diff;
          /**
            * 底部碰撞检测
          */
          if (this.y > bottom) {
            this.y = bottom
            this.vy *= -bounce;
          }
          /**
           * 左右边界超出检测
           * 将粒子从父对象的粒子库中删除
          */
          if (this.x < -width || this.x > right) {
            parent.remove(this)
          }
        }
      }
      // 绘图方法
      draw(ctx) {
        const {x,y,width,height} = this;
        ctx.save();
        ctx.fillRect(x,y,width,height);
        ctx.restore();
      }
    }

    // 粒子发射器
    class Gun {
      constructor(width, height) {
        // 尺寸
        this.width = width;
        this.height = height;
        // 位置
        this.x = 0;
        this.y = 0;
        // 状态 1: 枪膛中有粒子 0: 枪膛中没有粒子
        this._state = 0;
        // 粒子库
        this.children = [];
      }
      get state () {
        return this._state;
      }
      set state(val) {
        /**
         * 在为state赋值时，做简单的diff判断
         * 当现在赋予的值和过去的值不一样时:
         *  当现在赋予的值为1时，制造一个粒子
         *  当现在赋予的值为0时，发射粒子仓库中的第一个粒子为_state赋值
        */
        if(val !== this._state) {
          if (val) {
            this.createParticle();
          } else {
            this.children[0].state = 0;
          }
          this._state = val;
        }
      }
      createParticle() {
        const {x,y,width,height,children} = this;
        // 实例化粒子对象
        const part = new Particle(width, height);
        // 粒子位置
        part.x = x;
        part.y = y;
        // 指定粒子的父级
        part.parent = this;
        // 将粒子以前置的方式添加到粒子库中 unshift
        children.unshift(part);
      }
      remove(ele) {
        const {children} = this;
        const index = children.indexOf(ele);
        if (index !== -1) {
          children.splice(index, 1);
        }
      }
      update(diff) {
        this.children.forEach(ele => {
          ele.update(diff);
        })
      }
      // 绘制辅助线
      drawStroke(ctx) {
        const {x,y,width,height} = this;
        ctx.save();
        ctx.strokeStyle = '#aaa';
        ctx.strokeRect(x,y,width,height);
        ctx.restore();
      }
    }

    // 实例化粒子发射器
    const gun = new Gun(partSize, partSize);
    gun.x = width/2 -80;
    gun.y = 50;

    setInterval(function() {
      gun.state = Math.round(Math.random())
    },100);
    
    // 计时器
    let time = new Date();

    // 渲染
    !(function render(){
      // 时间差
      const diff = updateTime();
      // 更新粒子发射器
      gun.update(diff);
      // 清理画布
      ctx.clearRect(0,0,width,height);
      // 绘制边框
      gun.drawStroke(ctx);
      // 绘制粒子发射器里的粒子
      gun.children.forEach(ele => {
        ele.draw(ctx);
      })
      // 请求动画帧
      requestAnimationFrame(render);
    })();

    function updateTime() {
      const now = new Date();
      const diff = now - time;
      time = now;
      return diff;
    }

  </script>
</body>

</html>